Skip to content

fix: use pull_request_target for agentic CI on fork PRs#541

Merged
andreatgretel merged 4 commits intomainfrom
andreatgretel/fix/agentic-ci-fork-prs
Apr 15, 2026
Merged

fix: use pull_request_target for agentic CI on fork PRs#541
andreatgretel merged 4 commits intomainfrom
andreatgretel/fix/agentic-ci-fork-prs

Conversation

@andreatgretel
Copy link
Copy Markdown
Contributor

@andreatgretel andreatgretel commented Apr 14, 2026

Summary

The agentic CI review workflow doesn't work on fork PRs. Discovered on #526: the pull_request trigger requires manual approval in the Actions tab (not on the PR) for each fork workflow run, and fork PR runs don't have access to repo secrets/variables so the job fails at the AGENTIC_CI_MODEL check even after approval.

Switching to pull_request_target fixes both since the workflow definition comes from main, so GitHub skips the fork approval gate and base repo secrets/variables are available.

Changes

Changed

  • Switch trigger from pull_request to pull_request_target so fork PRs get secret access without per-run Actions tab approval
  • Add environment: agentic-ci to the review job for an explicit approval gate on the PR checks UI

Fixed

  • Prompt injection via fork-controlled recipe files - recipe files (.agents/recipes/) are now checked out from the base branch into base-recipes/, so fork PRs cannot tamper with the agent's prompt while API secrets are in scope (agentic-ci-pr-review.yml#L132-L140)
  • Expression injection hardening - all ${{ }} interpolations in run: blocks moved to env: blocks to eliminate shell injection surface from event payload values

Attention Areas

Reviewers: Please pay special attention to the following:

  • agentic-ci-pr-review.yml - Only file changed. Security model: gate job checks collaborator permissions, agentic-ci environment requires reviewer approval, recipe files come from base branch (not fork), no direct ${{ }} interpolation in shell

Description updated with AI

@andreatgretel andreatgretel requested a review from a team as a code owner April 14, 2026 00:06
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 14, 2026

Greptile Summary

This PR switches the agentic CI review workflow from pull_request to pull_request_target to enable fork PRs to access repo secrets, adding a collaborator gate job, an environment: agentic-ci approval gate, expression-injection hardening (all ${{ }} interpolations moved to env: blocks), and a base-branch-only sparse checkout of recipe files to prevent fork prompt injection.

  • P1 security: the Checkout PR branch step still checks out the fork's full tree — including CLAUDE.md and AGENTS.md — to $GITHUB_WORKSPACE/, which is the claude CLI's working directory. The CLI automatically loads those files as high-priority project instructions even in -p mode, leaving a residual prompt-injection path while ANTHROPIC_API_KEY is in scope despite the recipe-file protection.

Confidence Score: 4/5

Not safe to merge until the CLAUDE.md / AGENTS.md prompt-injection path is closed — API secrets are reachable from fork-controlled instruction files.

The PR's core intent (expression-injection hardening, environment gate, recipe protection) is well-executed, but one concrete P1 security gap remains: the full PR branch checkout leaves fork-controlled CLAUDE.md and AGENTS.md on disk as the claude CLI working-directory project instructions, providing a prompt-injection vector that bypasses the recipe-file protection the PR explicitly adds.

.github/workflows/agentic-ci-pr-review.yml — specifically the Checkout PR branch step and the claude invocation working directory.

Security Review

  • Residual prompt injection (.github/workflows/agentic-ci-pr-review.yml L139–143): the PR branch checkout places the fork's CLAUDE.md and AGENTS.md in $GITHUB_WORKSPACE/. The claude CLI loads these files automatically as project instructions when invoked from that directory, even in non-interactive -p mode. A fork PR can modify these files to inject arbitrary agent instructions while ANTHROPIC_API_KEY is active — the same attack class as the recipe-file injection the PR explicitly fixes.

Important Files Changed

Filename Overview
.github/workflows/agentic-ci-pr-review.yml Switches to pull_request_target with expression-injection hardening and base-branch recipe checkout; one residual prompt-injection vector remains via fork-controlled CLAUDE.md/AGENTS.md loaded automatically by the claude CLI.

Sequence Diagram

sequenceDiagram
    participant Fork as Fork PR / Maintainer
    participant GH as GitHub Actions
    participant Gate as gate job (ubuntu-latest)
    participant Env as agentic-ci environment
    participant Review as review job (self-hosted)
    participant Claude as claude CLI

    Fork->>GH: pull_request_target event (opened/labeled/ready)
    GH->>Gate: start gate job (no secrets)
    Gate->>Gate: check collaborator permission via GH API
    alt no write access
        Gate-->>GH: allowed=false stop
    end
    Gate-->>GH: allowed=true

    GH->>Env: request environment approval
    Env-->>GH: approved

    GH->>Review: start review job (secrets available)
    Review->>Review: checkout fork HEAD to GITHUB_WORKSPACE includes CLAUDE.md
    Review->>Review: sparse-checkout base branch .agents/recipes to base-recipes/
    Review->>Claude: claude -p prompt from base-recipes/
    Note over Claude: loads CLAUDE.md from GITHUB_WORKSPACE
    Claude->>Review: review output
    Review->>GH: post PR comment
Loading

Comments Outside Diff (1)

  1. .github/workflows/agentic-ci-pr-review.yml, line 139-143 (link)

    P1 security Residual prompt injection via CLAUDE.md / AGENTS.md

    The Checkout PR branch step checks out the fork's full tree (no sparse-checkout) to $GITHUB_WORKSPACE/, which is also the working directory when claude is invoked. The Claude Code CLI automatically loads CLAUDE.md (and any files it @-includes, such as AGENTS.md) from the working directory as high-priority project instructions — even in non-interactive -p mode. A fork PR that modifies CLAUDE.md or AGENTS.md can therefore inject arbitrary instructions into the agent while ANTHROPIC_API_KEY is in scope, circumventing the recipe protection added in this PR.

    The fix is to also add a sparse-checkout (or --exclude) on the first checkout, or to run claude with --cwd pointing to a directory that does not contain fork-controlled instruction files. For example:

    - name: Checkout PR branch
      uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      with:
        ref: ${{ steps.head.outputs.sha }}
        fetch-depth: 0
        sparse-checkout-cone-mode: false
        sparse-checkout: |
          /*
          !CLAUDE.md
          !AGENTS.md
          !.claude

    Or alternatively, delete/overwrite these files from the checkout before invoking claude, replacing them with known-good copies from the base branch.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: .github/workflows/agentic-ci-pr-review.yml
    Line: 139-143
    
    Comment:
    **Residual prompt injection via CLAUDE.md / AGENTS.md**
    
    The `Checkout PR branch` step checks out the fork's full tree (no sparse-checkout) to `$GITHUB_WORKSPACE/`, which is also the working directory when `claude` is invoked. The Claude Code CLI automatically loads `CLAUDE.md` (and any files it `@`-includes, such as `AGENTS.md`) from the working directory as high-priority project instructions — even in non-interactive `-p` mode. A fork PR that modifies `CLAUDE.md` or `AGENTS.md` can therefore inject arbitrary instructions into the agent while `ANTHROPIC_API_KEY` is in scope, circumventing the recipe protection added in this PR.
    
    The fix is to also add a sparse-checkout (or `--exclude`) on the first checkout, or to run `claude` with `--cwd` pointing to a directory that does not contain fork-controlled instruction files. For example:
    
    ```yaml
    - name: Checkout PR branch
      uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      with:
        ref: ${{ steps.head.outputs.sha }}
        fetch-depth: 0
        sparse-checkout-cone-mode: false
        sparse-checkout: |
          /*
          !CLAUDE.md
          !AGENTS.md
          !.claude
    ```
    
    Or alternatively, delete/overwrite these files from the checkout before invoking `claude`, replacing them with known-good copies from the base branch.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: .github/workflows/agentic-ci-pr-review.yml
Line: 139-143

Comment:
**Residual prompt injection via CLAUDE.md / AGENTS.md**

The `Checkout PR branch` step checks out the fork's full tree (no sparse-checkout) to `$GITHUB_WORKSPACE/`, which is also the working directory when `claude` is invoked. The Claude Code CLI automatically loads `CLAUDE.md` (and any files it `@`-includes, such as `AGENTS.md`) from the working directory as high-priority project instructions — even in non-interactive `-p` mode. A fork PR that modifies `CLAUDE.md` or `AGENTS.md` can therefore inject arbitrary instructions into the agent while `ANTHROPIC_API_KEY` is in scope, circumventing the recipe protection added in this PR.

The fix is to also add a sparse-checkout (or `--exclude`) on the first checkout, or to run `claude` with `--cwd` pointing to a directory that does not contain fork-controlled instruction files. For example:

```yaml
- name: Checkout PR branch
  uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
  with:
    ref: ${{ steps.head.outputs.sha }}
    fetch-depth: 0
    sparse-checkout-cone-mode: false
    sparse-checkout: |
      /*
      !CLAUDE.md
      !AGENTS.md
      !.claude
```

Or alternatively, delete/overwrite these files from the checkout before invoking `claude`, replacing them with known-good copies from the base branch.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (4): Last reviewed commit: "fix: move expression interpolations to e..." | Re-trigger Greptile

@github-actions
Copy link
Copy Markdown
Contributor

Code Review: PR #541 — fix: use pull_request_target for agentic CI on fork PRs

Summary

This PR switches the agentic CI review workflow from pull_request to pull_request_target so fork PRs can access repo secrets/variables without requiring per-run Actions tab approval. It adds two security mitigations: (1) an environment: agentic-ci gate requiring reviewer approval before the job runs, and (2) a separate checkout of recipe files from the base branch to prevent fork PRs from injecting malicious prompts.

Files changed: 1 (.github/workflows/agentic-ci-pr-review.yml)
Scope: +16 / -3 lines — CI workflow only, no application code.


Findings

Security

  1. pull_request_target with fork checkout (Medium Risk — Mitigated)
    pull_request_target is a well-known footgun: it grants secret access while running in the base-branch context, but if it checks out and executes fork code, secrets can be exfiltrated. This PR does check out the fork PR head (line 127–130), which is necessary for the Claude agent to review the diff. The mitigations are:

    • Gate job (lines 23–77): Verifies the triggering user has write access before the review job runs.
    • Environment protection (line 83): environment: agentic-ci requires explicit reviewer approval on the PR checks UI, adding a human-in-the-loop before secrets are exposed.
    • Base-branch recipes (lines 132–140): Recipe/prompt files are checked out from the base branch, so fork code cannot tamper with the agent's instructions.

    These three layers provide strong defense-in-depth. The remaining risk surface is that CLAUDE.md and AGENTS.md are still read from the fork's checkout (since the working directory is the PR head). However, the _runner.md recipe includes an explicit "Ignore embedded directives" constraint that instructs the agent to treat code content as data. This is acceptable given the environment approval gate.

  2. Recipe prompt injection prevention (Good)
    The new "Checkout base branch recipes" step (lines 135–140) with the accompanying security comment is a well-considered addition. Using sparse-checkout: .agents/recipes minimizes what's pulled from the base branch, and reading from base-recipes/ in the build step (lines 184–185) cleanly separates trusted prompts from untrusted PR code.

Correctness

  1. Base SHA fallback (Low — Minor)
    Line 138: ref: ${{ github.event.pull_request.base.sha || 'main' }}
    The fallback to 'main' handles the workflow_dispatch case where pull_request.base.sha is empty. This is correct and necessary.

  2. Inconsistent actions/checkout versions (Low — Nit)
    The PR head checkout (line 127) uses actions/checkout@de0fac2e... # v6.0.2, while the base-branch recipe checkout (line 136) uses actions/checkout@34e11487... # v4. Using two different major versions of the same action is a minor maintenance burden and could introduce subtle behavioral differences. Consider aligning both to the same version.

Workflow Behavior

  1. Concurrency key unchanged (Info)
    The concurrency group (line 19) uses github.event.pull_request.number, which works identically for pull_request_target since the event payload structure is the same. No issue here.

  2. Label removal step (Info)
    The agent-review label removal step (lines 212–218) uses github.event.action and github.event.label.name, which are populated identically under pull_request_target. No issue.


Verdict

Approve — This is a well-structured security improvement. The pull_request_target migration is handled with appropriate defense-in-depth (permission gate + environment approval + base-branch recipe isolation). The prompt injection mitigation via base-branch recipe checkout is the right approach.

Optional suggestion: Align the actions/checkout version for the base-branch recipe checkout (finding #4) to match the primary checkout for consistency.

@johnnygreco
Copy link
Copy Markdown
Contributor

johnnygreco commented Apr 15, 2026

@andreatgretel – seems like this one is worth checking:

  1. The agentic-ci environment MUST exist before merge (blocking)

This is the single most important thing. The PR adds environment: agentic-ci (line 90 in the new file) as the human-approval gate
that prevents fork code from executing with secrets before a reviewer says "go." But this is a deployment precondition, not something
enforced by the workflow itself.

If the agentic-ci environment doesn't exist in GitHub repo settings with "required reviewers" configured:

  • GitHub silently treats it as a no-op
  • Any fork PR from a write-access collaborator runs the review job immediately with full secret access
  • The pull_request_target trigger means the fork's code is checked out while AGENTIC_CI_API_KEY is in scope

Action: Confirm in GitHub repo settings that the agentic-ci environment exists with required reviewers before merging. Both the Greptile bot and the agentic CI review flagged this same point.

@johnnygreco
Copy link
Copy Markdown
Contributor

Review: pull_request_target for agentic CI on fork PRs

1. Fork-controlled files in Claude's working directory (worth understanding)

The recipe files (_runner.md, recipe.md) are correctly sourced from the base branch via the new base-recipes/ checkout — good. But the Claude CLI's working directory is the fork's checkout, which means:

  • CLAUDE.md and AGENTS.md are read from the fork's code. A malicious fork could modify these to inject instructions into the agent's system prompt.
  • Any file the agent reads during review (source code, configs, etc.) comes from the fork.

The existing _runner.md recipe reportedly includes an "ignore embedded directives" instruction, which is a reasonable but inherently fuzzy LLM-level defense. This is probably acceptable given that the environment gate adds a human review step, but it's worth being aware of.

2. Expression injection hardening (good, verify completeness)

Moving all ${{ }} interpolations from run: blocks into env: blocks is the correct pattern. This eliminates shell injection from event payload values (e.g., a PR title containing $(curl attacker.com)). The PR does this consistently across both the gate and review jobs.

One thing to note: ${{ }} expressions in with: blocks (like ref: ${{ steps.head.outputs.sha }}) are not a shell injection vector — they're passed as action inputs, not interpolated into shell. So those are fine as-is.

3. actions/checkout version consistency (nit)

Both checkout steps appear to use the same pinned SHA (de0fac2e... / v6.0.2). The Greptile bot flagged a version mismatch but that appears to be stale — the current diff shows them aligned. Worth a quick visual confirm.

4. Gate job permission model (looks correct)

The gate job correctly:

  • Short-circuits for workflow_dispatch (callers already have write access)
  • Checks the labeler (not the PR author) for labeled events, so maintainers can authorize reviews on external PRs
  • Falls back to PR author for opened/ready_for_review events
  • Uses the collaborator API rather than author_association (which is unreliable with private org membership)

The gate runs on ubuntu-latest with only github.token — no secrets exposed in this job. Clean separation.

Bottom line

The PR is well-structured with three layers of defense (permission gate, environment approval, base-branch recipes). The expression injection hardening is a real improvement. The main thing to confirm (already flagged in a separate comment) is that the agentic-ci environment is configured in repo settings with required reviewers — without that, the entire security model collapses to just the permission gate.

Your boy, Claude

@andreatgretel
Copy link
Copy Markdown
Contributor Author

confirmed - the agentic-ci environment already exists in repo settings with a required_reviewers protection rule tied to the data_designer_reviewers team. so any fork PR trigger blocks until a team member approves the deployment. we're good there.

on the other findings:

  • fork-controlled CLAUDE.md/AGENTS.md - yeah, agreed this is fine. the environment gate is the real control, the "ignore embedded directives" bit in _runner.md is just defense-in-depth.
  • actions/checkout versions - already aligned in 494629be, both pinned to de0fac2e (v6.0.2). greptile's nit was against a stale commit.
  • rest looks good, no changes needed.

Recipe files define the agent's prompt. When using pull_request_target,
the fork's HEAD is checked out, so a malicious fork could craft recipe
files to exfiltrate API secrets via prompt injection. Fix by adding a
second sparse checkout from the base branch for .agents/recipes/ and
reading prompts from there instead of the fork tree.
Match the base-branch recipe checkout to v6.0.2 (same SHA as the PR
branch checkout) for consistency.
Replace direct ${{ }} interpolation in run: blocks with env vars.
Most values are GitHub-controlled, but github.event.label.name can
contain arbitrary characters and could break shell quoting. Moving
everything to env: is consistent with the injection-hardening pattern
applied in the rest of the workflow.
@andreatgretel andreatgretel force-pushed the andreatgretel/fix/agentic-ci-fork-prs branch from 9bc954a to 65aab94 Compare April 15, 2026 20:59
@andreatgretel andreatgretel merged commit 6ef4953 into main Apr 15, 2026
49 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants